/*
 * FindReplacePanel
 * Copyright (C) 2011, Aleksey Nikiforov
 *
 * This file is part of FindReplacePanel.
 *
 * FindReplacePanel is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * FindReplacePanel is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package simplex3d.console.findreplace;

import java.awt.Color;
import java.awt.event.*;
import java.util.HashMap;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.prefs.Preferences;
import javax.swing.*;
import javax.swing.text.BadLocationException;
import javax.swing.text.Caret;
import javax.swing.text.JTextComponent;


/**
 *
 * @author Aleksey Nikiforov (lex)
 */
public class FindReplacePanel extends JPanel {

    public static final int DIALOG_FIND = 0;
    public static final int DIALOG_REPLACE = 1;

    public static final String CMD_MATCH_CASE = "FindReplacePanel.MatchCaseCmd";
    public static final String CMD_WHOLE_WORDS = "FindReplacePanel.WholeWordsCmd";
    public static final String CMD_REGULAR_EXPRESSION = "FindReplacePanel.RegexCmd";
    public static final String CMD_HIGHLIGHT = "FindReplacePanel.HighlightCmd";
    public static final String CMD_HIDE = "FindReplacePanel.HideCmd";
    
    public static final String CMD_FIND_NEXT = "FindReplacePanel.NextCmd";
    public static final String CMD_FIND_PREVIOUS = "FindReplacePanel.PreviousCmd";
    public static final String CMD_REPLACE = "FindReplacePanel.ReplaceCmd";
    public static final String CMD_REPLACE_ALL = "FindReplacePanel.ReplaceAllCmd";

    public static final String CMD_SEARCH_TYPED = "FindReplacePanel.SearchTypedCmd";

    public static final String CMD_SHOW_FIND_DIALOG = "FindReplacePanel.ShowFindDialogCmd";
    public static final String CMD_SHOW_REPLACE_DIALOG = "FindReplacePanel.ShowReplaceDialogCmd";

    public static final String CMD_UNDO = "FindReplacePanel.UndoCmd";
    public static final String CMD_REDO = "FindReplacePanel.RedoCmd";

    static final boolean DEFAULT_MATCH_CASE_SELECTED = true;
    static final boolean DEFAULT_WHOLE_WORDS_SELECTED = false;
    static final boolean DEFAULT_REGULAR_EXPRESSION_SELECTED = false;
    static final boolean DEFAULT_HIGHLIGHT_ALL_SELECTED = true;
    static final Color DEFAULT_WORK_AREA_COLOR = new Color(0.0f, 0.0f, 0.0f, 0.15f);
    static final Color DEFAULT_RESULT_HIGHLIGHT_COLOR = new Color(1.0f, 0.7f, 0.26f, 0.7f);


    private JScrollPane scrollPane;
    private JTextComponent textComponent;

    private final Action showFindDialogAction;
    private final Action showReplaceDialogAction;
    private final Action hideAction;
    private final Action highlightAction;

    private final Action findNextAction;
    private final Action findPreviousAction;
    private final Action replaceAction;
    private final Action replaceAllAction;

    private final Action searchTypedAction;

    private final SearchDialog dialog;
    private final WorkArea workArea;
    private final SearchResultHighlighter highlighter;

    private final SearchQuery searchQuery = new SearchQuery();


    public FindReplacePanel() {
        this(null, new JTextArea(), new JScrollPane(), JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
    }

    public FindReplacePanel(Preferences prefs, JTextComponent jTextComponent, JScrollPane jScrollPane) {
        this(prefs, jTextComponent, jScrollPane, JComponent.WHEN_IN_FOCUSED_WINDOW);
    }

    /** Main constructor.
     * Use TempPreferences provided in the package if you do not wish to store search options and history.
     *
     * @param preferences persistent or temporary preferences.
     * @param jTextComponent text component to embed.
     * @param jScrollPane scrollPane for the text component.
     * @param hotkeyLevel usually JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT.
     */
    public FindReplacePanel(
        Preferences preferences,
        JTextComponent jTextComponent, JScrollPane jScrollPane,
        int hotkeyLevel
    ) {

        initComponents();
        final ResourceBundle bundle = ResourceBundle.getBundle("simplex3d/console/findreplace/Bundle");


        // Init textComponent.
        textComponent = jTextComponent;
        workArea = new WorkArea(textComponent, new AbstractAction() {
            public void actionPerformed(ActionEvent e) {
                updateSearchQuery(true);
                updateResultHighlighter();
            }
        });
        highlighter = new SearchResultHighlighter(textComponent);
        

        // Init scrollPane
        if (jScrollPane == null) {
            add(textComponent, java.awt.BorderLayout.CENTER);
        }
        else {
            scrollPane = jScrollPane;
            scrollPane.setViewportView(textComponent);
            add(scrollPane, java.awt.BorderLayout.CENTER);
        }

        
        // Configure options actions.
        Map<String, Action> optionsActions = OptionsComponent.makeActions(preferences);

        highlightAction = new ToggleAction(
            "OptionsComponent.highlightCheckBox",
            bundle, FindReplacePanel.CMD_HIGHLIGHT, preferences,
            FindReplacePanel.DEFAULT_HIGHLIGHT_ALL_SELECTED
        ) {
            @Override
            public void actionPerformed(ActionEvent e) {
                super.actionPerformed(e);
                updateResultHighlighter();
            }
        };

        final String keyHide = "OptionsComponent.hideButton";
        hideAction = new AbstractAction(bundle.getString(keyHide + ".text")) {
            {
                putValue(MNEMONIC_KEY, (int) bundle.getString(keyHide + ".mnemonic").charAt(0));
                putValue(SHORT_DESCRIPTION, bundle.getString(keyHide + ".tooltip"));
                putValue(ACTION_COMMAND_KEY, FindReplacePanel.CMD_HIDE);
            }

            public void actionPerformed(ActionEvent e) {
                // must be invoked before disableWorkArea() to affect highlightAll option.
                dialog.setEnabled(false);
                disableWorkArea();
                textComponent.requestFocus();
            }
        };

        optionsActions.put(CMD_HIGHLIGHT, highlightAction);
        optionsActions.put(CMD_HIDE, hideAction);



        // Configure Find/Replace actions.
        final String keyFindNext = "FindReplaceComponent.findNextButton";
        findNextAction = new AbstractAction(bundle.getString(keyFindNext + ".text")) {
            {
                putValue(MNEMONIC_KEY, (int) bundle.getString(keyFindNext + ".mnemonic").charAt(0));
                putValue(SHORT_DESCRIPTION, bundle.getString(keyFindNext + ".tooltip"));
                putValue(ACTION_COMMAND_KEY, CMD_FIND_NEXT);
            }

            public void actionPerformed(ActionEvent e) {
                dialog.saveHistory();

                updateSearchQuery(false);
                updateResultHighlighter();

                if (searchQuery.canSearch()) {
                    Caret caret = textComponent.getCaret();
                    SearchQuery.Result res = searchQuery.findNext(caret.getDot());
                    if (res != null) {
                        caret.setDot(res.from);
                        caret.moveDot(res.to);
                        
                        // Force text selection on non-focusted components.
                        textComponent.getCaret().setSelectionVisible(true);
                    }
                }
            }
        };

        final String keyFindPrevious = "FindReplaceComponent.findPreviousButton";
        findPreviousAction = new AbstractAction(bundle.getString(keyFindPrevious + ".text")) {
            {
                putValue(MNEMONIC_KEY, (int) bundle.getString(keyFindPrevious + ".mnemonic").charAt(0));
                putValue(SHORT_DESCRIPTION, bundle.getString(keyFindPrevious + ".tooltip"));
                putValue(ACTION_COMMAND_KEY, CMD_FIND_PREVIOUS);
            }

            public void actionPerformed(ActionEvent e) {
                dialog.saveHistory();

                updateSearchQuery(false);
                updateResultHighlighter();

                if (searchQuery.canSearch()) {
                    Caret caret = textComponent.getCaret();
                    int pos = Math.min(caret.getDot(), caret.getMark());

                    SearchQuery.Result res = searchQuery.findPrevious(pos);
                    if (res != null) {
                        caret.setDot(res.from);
                        caret.moveDot(res.to);

                        // Force text selection on non-focusted components.
                        textComponent.getCaret().setSelectionVisible(true);
                    }
                }
            }
        };

        final String keyReplace = "ReplaceComponent.replaceButton";
        replaceAction = new AbstractAction(bundle.getString(keyReplace + ".text")) {
            {
                putValue(MNEMONIC_KEY, (int) bundle.getString(keyReplace + ".mnemonic").charAt(0));
                putValue(SHORT_DESCRIPTION, bundle.getString(keyReplace + ".tooltip"));
                putValue(ACTION_COMMAND_KEY, CMD_REPLACE);
            }

            public void actionPerformed(ActionEvent e) {
                dialog.saveHistory();
                SearchParams params = dialog.getSearchParams();

                updateSearchQuery(false);

                if (searchQuery.canSearch() && searchQuery.getParams().euqalsAsMatcher(params)) {
                    Caret caret = textComponent.getCaret();
                    int pos = Math.min(caret.getDot(), caret.getMark());

                    SearchQuery.Result res = searchQuery.findNext(pos);
                    if (
                        res != null &&
                        res.from == textComponent.getSelectionStart() &&
                        res.to == textComponent.getSelectionEnd()
                    ) {
                        int wfrom = workArea.getFrom();
                        boolean empty = workArea.isEmpty();
                        boolean inv = workArea.wasIntertedSelection();

                        String replaced = searchQuery.replaceNext(res.from, res.to);
                        replaceText(res.from, res.to, replaced);

                        // Fixes first word being lost from work selection when replaced.
                        if (!empty) {
                            if (inv) workArea.set(workArea.getTo(), wfrom);
                            else workArea.set(wfrom, workArea.getTo());
                        }
                    }
                }
                
                findNextAction.actionPerformed(e);
            }
        };

        final String keyReplaceAll = "ReplaceComponent.replaceAllButton";
        replaceAllAction = new AbstractAction(bundle.getString(keyReplaceAll + ".text")) {
            {
                putValue(MNEMONIC_KEY, (int) bundle.getString(keyReplaceAll + ".mnemonic").charAt(0));
                putValue(SHORT_DESCRIPTION, bundle.getString(keyReplaceAll + ".tooltip"));
                putValue(ACTION_COMMAND_KEY, CMD_REPLACE_ALL);
            }

            public void actionPerformed(ActionEvent e) {
                dialog.saveHistory();

                updateSearchQuery(false);

                if (searchQuery.canSearch()) {
                    SearchParams params = searchQuery.getParams();

                    int from = workArea.getFrom();
                    int to = workArea.getTo();
                    boolean empty = workArea.isEmpty();
                    boolean inv = workArea.wasIntertedSelection();

                    if (workArea.isEmpty()) {
                        from = 0;
                        to = textComponent.getDocument().getLength();
                    }

                    // Fixes the caret pos.
                    int dot = textComponent.getCaret().getDot();
                    String repToDot = replaceToDot(dot);
                    if (repToDot != null) dot = from + repToDot.length();

                    String replaced = searchQuery.replaceAll();
                    replaceText(from, to, replaced);

                    textComponent.getCaret().setDot(dot);

                    // Restores work selection.
                    if (!empty) {
                        if (inv) workArea.set(from + replaced.length(), from);
                        else workArea.set(from, from + replaced.length());
                    }
                }
            }
        };

        searchTypedAction = new AbstractAction() {
            public void actionPerformed(ActionEvent e) {
                updateSearchQuery(false);
                updateResultHighlighter();
            }
        };


        Map<String, Action> findReplaceActions = new HashMap<String, Action>(4);
        findReplaceActions.put(CMD_FIND_NEXT, findNextAction);
        findReplaceActions.put(CMD_FIND_PREVIOUS, findPreviousAction);
        findReplaceActions.put(CMD_REPLACE, replaceAction);
        findReplaceActions.put(CMD_REPLACE_ALL, replaceAllAction);
        findReplaceActions.put(CMD_SEARCH_TYPED, searchTypedAction);


        // Configure the show dialog actions.
        final String keyShowFind = "FindReplacePanel.showFindMenuItem";
        showFindDialogAction = new AbstractAction(bundle.getString(keyShowFind + ".text")) {
            {
                putValue(MNEMONIC_KEY, (int) bundle.getString(keyShowFind + ".mnemonic").charAt(0));
                putValue(ACTION_COMMAND_KEY, CMD_SHOW_FIND_DIALOG);
            }

            public void actionPerformed(ActionEvent e) {
                setWorkArea();
                String searchTarget = useSelectionAsSearchTarget() ? getSelectedText() : null;
                dialog.show(DIALOG_FIND, searchTarget);
                findNextAction.actionPerformed(e);
            }
        };

        final String keyShowReplace = "FindReplacePanel.showReplaceMenuItem";
        showReplaceDialogAction = new AbstractAction(bundle.getString(keyShowReplace + ".text")) {
            {
                putValue(MNEMONIC_KEY, (int) bundle.getString(keyShowReplace + ".mnemonic").charAt(0));
                putValue(ACTION_COMMAND_KEY, CMD_SHOW_FIND_DIALOG);
            }

            public void actionPerformed(ActionEvent e) {
                setWorkArea();
                
                String searchTarget = useSelectionAsSearchTarget() ? getSelectedText() : null;
                if (dialog.show(DIALOG_REPLACE, searchTarget)) {
                    findNextAction.actionPerformed(e);
                }
                else {
                    replaceAction.actionPerformed(e);
                }
            }
        };


        // Configure Panel Keys.
        KeyStroke showFindDialogKey = KeyStroke.getKeyStroke(KeyEvent.VK_F, InputEvent.CTRL_DOWN_MASK, false);
        KeyStroke showReplaceDialogKey = KeyStroke.getKeyStroke(KeyEvent.VK_R, InputEvent.CTRL_DOWN_MASK, false);
        KeyStroke hideDialogKey = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0, true);
        KeyStroke findNextKey = KeyStroke.getKeyStroke(KeyEvent.VK_F3, 0, false);
        KeyStroke findPreviousKey = KeyStroke.getKeyStroke(KeyEvent.VK_F3, InputEvent.SHIFT_DOWN_MASK, false);
        KeyStroke undoKey = KeyStroke.getKeyStroke(KeyEvent.VK_Z, InputEvent.CTRL_DOWN_MASK, false);
        KeyStroke redoKey = KeyStroke.getKeyStroke(KeyEvent.VK_Y, InputEvent.CTRL_DOWN_MASK, false);
        {
            InputMap inputMap = getInputMap(hotkeyLevel);
            inputMap.put(showFindDialogKey, CMD_SHOW_FIND_DIALOG);
            inputMap.put(showReplaceDialogKey, CMD_SHOW_REPLACE_DIALOG);
            inputMap.put(hideDialogKey, CMD_HIDE);
            inputMap.put(findNextKey, CMD_FIND_NEXT);
            inputMap.put(findPreviousKey, CMD_FIND_PREVIOUS);
            inputMap.put(undoKey, CMD_UNDO);
            inputMap.put(redoKey, CMD_REDO);

            ActionMap actionMap = getActionMap();
            actionMap.put(CMD_SHOW_FIND_DIALOG, showFindDialogAction);
            actionMap.put(CMD_SHOW_REPLACE_DIALOG, showReplaceDialogAction);
            actionMap.put(CMD_HIDE, hideAction);
            actionMap.put(CMD_FIND_NEXT, findNextAction);
            actionMap.put(CMD_FIND_PREVIOUS, findPreviousAction);
            actionMap.put(CMD_UNDO, workArea.getUndoAction());
            actionMap.put(CMD_REDO, workArea.getRedoAction());
        }


        dialog = new SearchDialog(this, findReplaceActions, optionsActions, preferences);
    }


    private void replaceText(int from, int to, String replacement) {
        try {
            textComponent.getDocument().remove(from, to - from);
            textComponent.getDocument().insertString(from, replacement, null);
        } catch (BadLocationException ignore) {}
    }

    private void updateSearchQuery(boolean updateText) {
        searchQuery.setParams(dialog.getSearchParams());

        if (updateText) {
            String text = textComponent.getText();
            int from = workArea.getFrom();
            int to = workArea.getTo();
            if (workArea.isEmpty()) {
                from = 0;
                to = text.length();
            }
            searchQuery.setText(text, from, to);
        }
    }

    private String replaceToDot(int dot) {
        if (!searchQuery.canSearch()) return null;
        else if (dot < searchQuery.getFrom() || dot >= searchQuery.getTo()) return null;
        else {
            int qto = searchQuery.getTo();
            searchQuery.setText(textComponent.getText(), searchQuery.getFrom(), dot);
            String replaced = searchQuery.replaceAll();
            searchQuery.setText(textComponent.getText(), searchQuery.getFrom(), qto);
            return replaced;
        }
    }

    private void updateResultHighlighter() {
        if (searchQuery.canSearch() && searchQuery.getParams().highlight) {
            highlighter.setHighlights(searchQuery.getAllResults());
        }
        else {
            highlighter.removeHighlights();
        }
    }

    private boolean useSelectionAsSearchTarget() {
        String text = getSelectedText();
        return textComponent.isFocusOwner() && (text.length() > 0) && !(text.contains("\n"));
    }

    private String getSelectedText() {
        String selected = textComponent.getSelectedText();
        if (selected == null) return "";
        else return selected;
    }

    private void setWorkArea() {
        String selected = getSelectedText();
        Caret c = textComponent.getCaret();

        if (selected.contains("\n")) {
            workArea.set(c.getMark(), c.getDot());
            c.setDot(workArea.getFrom());
        }
        else if (workArea.isEmpty()) {
            int dot = c.getDot();
            workArea.set(dot, dot);
        }

        updateSearchQuery(true);
        updateResultHighlighter();
    }

    private void disableWorkArea() {
        Caret c = textComponent.getCaret();

        // Set selection to working area.
//        if (!workArea.isEmpty()) {
//            if (!workArea.wasIntertedSelection()) {
//                c.setDot(workArea.getFrom());
//                c.moveDot(workArea.getTo());
//            } else {
//                c.setDot(workArea.getTo());
//                c.moveDot(workArea.getFrom());
//            }
//        }

        workArea.set(0, 0);

        updateSearchQuery(true);
        updateResultHighlighter();
    }

    public void setUndoForwards(Action undo, Action redo) {
        workArea.setUndoForwards(undo, redo);
    }

    
    // Getters and setters.
    public JScrollPane getScrollPane() {
        return scrollPane;
    }

    public JTextComponent getTextComponent() {
        return textComponent;
    }

    public Action getShowFindDialogAction() {
        return showFindDialogAction;
    }

    public Action getShowReplaceDialogAction() {
        return showReplaceDialogAction;
    }

    public Action getHideAction() {
        return hideAction;
    }

    public Action getHighlightToggleAction() {
        return highlightAction;
    }

    public Action getFindNextAction() {
        return findNextAction;
    }

    public Action getFindPreviousAction() {
        return findPreviousAction;
    }

    public Action getReplaceAction() {
        return replaceAction;
    }

    public Action getReplaceAllAction() {
        return replaceAllAction;
    }

    public Action getUndoAction() {
        return workArea.getUndoAction();
    }

    public Action getRedoAction() {
        return workArea.getRedoAction();
    }

    public Color getSearchAreaColor() {
        return workArea.getColor();
    }

    public void setSearchAreaColor(Color color) {
        workArea.setColor(color);
    }

    public Color getSearchResultColor() {
        return highlighter.getColor();
    }

    public void setSearchResultColor(Color color) {
        highlighter.setColor(color);
    }


    public void addActionListener(ActionListener a) {
        dialog.addActionListener(a);
    }

    public void removeActionListener(ActionListener a) {
        dialog.removeActionListener(a);
    }


    /** This method is called from within the constructor to
     * initialize the form.
     * WARNING: Do NOT modify this code. The content of this method is
     * always regenerated by the Form Editor.
     */
    @SuppressWarnings("unchecked")
    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
    private void initComponents() {

        setLayout(new java.awt.BorderLayout());
    }// </editor-fold>//GEN-END:initComponents


    // Variables declaration - do not modify//GEN-BEGIN:variables
    // End of variables declaration//GEN-END:variables

}
